using System;
using System.Collections.Generic;
using System.Text;

using System.Net;
using System.Net.Sockets;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;

namespace Space_Conquest
{

    public delegate void ReceivedDataEventHandler(object o);

    /// <summary>
    /// Base class for Server and Client classes. Defines basic functionality that both Server and Client have
    /// </summary>
    public abstract class SocketConnection
    {

        #region SocketPacket

        /// <summary>
        /// An internally used class, holds data buffer for transfers
        /// </summary>
        class SocketPacket
        {

            public SocketPacket()
            {
                DataBuffer = new byte[4];
                IsMeta = true;
            }

            public SocketPacket(int numOfBytes)
            {
                DataBuffer = new byte[numOfBytes];
                IsMeta = false;
            }

            public bool IsMeta;
            public System.Net.Sockets.Socket CurrentSocket;
            public byte[] DataBuffer;

        } 

        #endregion

        #region Private Members

        protected delegate void ConnectionErrorEventHandler(Socket source);
        protected event ConnectionErrorEventHandler OnConnectionError;
        protected int m_port;

        protected Socket m_mainSocket;
	    
        #endregion

        #region Properties

        public event ReceivedDataEventHandler OnReceiveData;

        public int Port
        {
            get
            {
                return m_port;
            }
        }

        #endregion

        #region Constructor

        public SocketConnection()
        {

            // Initialize Main Socket
            m_mainSocket = new Socket(AddressFamily.InterNetwork,
                                      SocketType.Stream,
                                      ProtocolType.Tcp);

            // This means it can quickly send small amounts of data
            m_mainSocket.NoDelay = true;

        }

        #endregion

        #region Methods

        public virtual void SendData(object obj)
        {
        }

        protected void WaitForMeta(Socket waitingSocket)
        {

            try
            {

                SocketPacket theSocPkt = new SocketPacket();
                theSocPkt.CurrentSocket = waitingSocket;

                waitingSocket.BeginReceive(theSocPkt.DataBuffer, 0,
                                   theSocPkt.DataBuffer.Length,
                                   SocketFlags.None,
                                   new AsyncCallback(OnDataReceived),
                                   theSocPkt);


            }
            catch (SocketException se)
            {
                Console.WriteLine(se.Message);
            }

        }

        private void WaitForData(Socket waitingSocket, int numOfBytes)
        {

            try
            {

                SocketPacket theSocPkt = new SocketPacket(numOfBytes);
                theSocPkt.CurrentSocket = waitingSocket;

                waitingSocket.BeginReceive(theSocPkt.DataBuffer, 0,
                                   theSocPkt.DataBuffer.Length,
                                   SocketFlags.None,
                                   new AsyncCallback(OnDataReceived),
                                   theSocPkt);


            }
            catch (SocketException se)
            {
                Console.WriteLine(se.Message);
            }

        }


        protected void OnDataReceived(IAsyncResult asyn)
        {

            SocketPacket theSockId = (SocketPacket)asyn.AsyncState;

            try
            {
                theSockId.CurrentSocket.EndReceive(asyn);

                if (theSockId.IsMeta)
                {

                    // Got meta, use to wait for data
                    WaitForData(theSockId.CurrentSocket,
                                BitConverter.ToInt32(theSockId.DataBuffer, 0));
                }
                else
                {
                    // Got data, return object and wait for meta

                    object temp = Deserialize(theSockId.DataBuffer);

                    SendReceiveEvent(temp);

                    WaitForMeta(theSockId.CurrentSocket);
                }
            }
            catch
            {
                if (this is Server)
                {
                    // Either a client disconnected, or server shut down, handle separately
                    if (OnConnectionError != null)
                    {
                        OnConnectionError(theSockId.CurrentSocket);
                    }
                }
            }

        }

        protected virtual void SendReceiveEvent(object obj)
        {

            if (OnReceiveData != null)
                OnReceiveData(obj);

        }

        /// <summary>
        /// Same as Serialize, except adds four bytes on beginning indicating size of object (only use for these classes)
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        protected byte[] FullSerialize(object obj)
        {


            byte[] arr = Serialize(obj);
            byte[] full = new byte[4 + arr.Length];

            BitConverter.GetBytes(arr.Length).CopyTo(full, 0);
            arr.CopyTo(full, 4);

            return full;
        
        }

        /// <summary>
        /// Take an object and serialize it into a byte array
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        protected byte[] Serialize(object obj)
        {

            BinaryFormatter binaryFormatter = new BinaryFormatter();
            MemoryStream memoryStream = new MemoryStream();

            binaryFormatter.Serialize(memoryStream, obj);
            memoryStream.Seek(0, SeekOrigin.Begin);
            byte[] arr = new byte[(int)memoryStream.Length];
            memoryStream.Read(arr, 0, (int)memoryStream.Length);

            return arr;

        }

        /// <summary>
        /// Take a byte array and deserialize it into an object
        /// </summary>
        /// <param name="data"></param>
        /// <returns></returns>
        protected object Deserialize(byte[] data)
        {

            BinaryFormatter binaryFormatter = new BinaryFormatter();
            MemoryStream memoryStream = new MemoryStream();

            memoryStream.Write(data, 0, data.Length);
            memoryStream.Seek(0, SeekOrigin.Begin);
            return binaryFormatter.Deserialize(memoryStream);

        }

        #endregion

    }
}
